﻿<!doctype html />
<html>
    <head>
        <title>testGetEvents</title>
    </head>
    <body>
        <img src="onepixel.png" />
        <div id="explanation">Known Limitations: 
            <ol>
                <li>Only downloads the next 100 elements from the moment you loaded the page. To load more events, (God forbid), reload the page. But not all the time, just when a couple of events have passed since you started the page.</li>
                <li>The tickmarks for the hours might be off. I'm looking into it.</li>
            </ol>
            Feedback is very welcome: <a href='mailto:herrturtur@live.com'>herrturtur@live.com</a>
        </div>
        <canvas id="canvas" width="800" height="500"></canvas>
        <input type="range" id="range" min="0" max="100" step="1" value="50"/>
		<label for="range" id="label">Slide me :)</label>
    </body>

    <script type="text/javascript">
        RUN_TESTS = false;

        function run_test(foo){
            if(RUN_TESTS){
                var result = foo();
                console.log((result ? 'OK' : 'FAIL') + ' ' + foo.name);
            }
        }

        function drawTick(x, text, event, callback, textXoffset, textYoffset) {
            context.beginPath();
            context.moveTo(x, canvas.height / 2 - HALFTICKLENGTH);
            context.lineTo(x, canvas.height / 2 + HALFTICKLENGTH);
            drawText(text, x + textXoffset, canvas.height / 2 + HALFTICKLENGTH + 2 + textYoffset, event, callback);

            context.stroke();
            context.closePath();
        }

        function Record(shape, x1, y1, x2, y2, event, callback) {
            this.shape = shape;
            this.x1 = x1;
            this.y1 = y1;
            this.x2 = x2;
            this.y2 = y2;
            this.event = event;
            this.callback = callback;
        }

        function RectRecord(shape, x, y, width, height, event, callback) {
            this.shape = shape;
            this.x = x;
            this.y = y;
            this.width = width;
            this.height = height;
            this.event = event;
            this.callback = callback;
        }

        function RecordKeeper() {
            RecordKeeper.pointOnRectangle = function (pointX, pointY, rectX, rectY, width, height) {
                if ((width > 0 ? pointX >= rectX && pointX <= rectX + width : pointX <= rectX && pointX >= rectX + width) &&
                    (height > 0 ? pointY >= rectY && pointY <= rectY + height : pointY <= rectY && pointY >= rectY + height)) {
                    return true;
                }
                return false;
            }

            RecordKeeper.pointOnLine = function (pointX, pointY, lineX1, lineY1, lineX2, lineY2) {
                if (pointX >= lineX1 && pointX <= lineX2 && pointY >= lineY1 && pointY <= lineY2) {
                    return true;
                }

                return false;
            }

            RecordKeeper.records = [];
            RecordKeeper.push = function (item) {
                RecordKeeper.records.push(item);
            };

            RecordKeeper.clear = function () {
                RecordKeeper.records.length = 0;
            };

            document.onclick = function (e) {
                //console.log(e.pageX + ', ' + e.pageY);
                for (var i = RecordKeeper.records.length - 1; i >= 0; i--) {
                    var record = RecordKeeper.get(i);
                    //console.log('record: ' + record.event.name + '(' + record.x + ',' + record.y + '), (' + record.width + ', ' + record.height + ')');
                    if (record.shape == 'rectangle')
                        if (RecordKeeper.pointOnRectangle(e.pageX, e.pageY, record.x, record.y, record.width, record.height)) {
                            record.callback(record);
                            return;
                        }
                }
            };

            RecordKeeper.length = function () {
                return RecordKeeper.records.length;
            };

            RecordKeeper.get = function (i) {
                return RecordKeeper.records[i];
            };
        }

        function Event(start, end, name, text) {
            this.start = start;
            this.end = end;
            this.name = name;
            this.text = text;
        }

        /*
            Background class. This class is responsible for supplying events to calendar before
            they are drawn. To use, simply call class definition, and then its member methods.
            This class functions like a Priority Queue, where priority is indicated by the 
            start time.
            
            methods:
            EventCollection(),
            EventCollection.get(i), retrieve the ith event. No bounds checking done inside.
            EventCollection.size(), returns the number of items in the EventCollection
            EventCollection.remove(i), remove the ith member. No bounds checking done inside.
        */
        function EventCollection() {
            EventCollection.events = [];
            EventCollection.EVENT_INDEX = 0;
            EventCollection.LEVEL_INDEX = 1;
            /*
                EventCollection.push() inserts a value into the EventCollection at level 0.
            */
            EventCollection.push = function push(event) {

                // find position for event
                var pos = 0;
                for (var i = 0; i < EventCollection.size() && EventCollection.get(i).start < EventCollection.get(i).start; i++)
                    pos = i;

                // insert event
                eventData = new Array();
                eventData.push(event);
                eventData.push(0);
                EventCollection.events.splice(pos, 0, eventData);
            }

            EventCollection.get = function getEvent(i) {
                return EventCollection.events[i][EventCollection.EVENT_INDEX];
            }

            EventCollection.size = function() {
                return EventCollection.events.length;
            }

            EventCollection.remove = function (i) {
                EventCollection.events.splice(i, 1);
            }

            /*
            This function returns an integer, to be used for determining how far away from the number line
            an event should be drawn.
            The goal here is to have the closest event at the bottom, stacking events on top,
            if they overlap and simply placing them next to each other when they're after
            each other.
            Very little bounds checking is done here. Don't rely on it.
            min value: 1
            */
            EventCollection.getLevel = function (i) {
                return EventCollection.events[i][EventCollection.LEVEL_INDEX];
            }

            EventCollection._eventShown = function (i) {
                var starts_after_now = EventCollection.get(i).start >= now.getTime();
                var starts_before_end = EventCollection.get(i).start <= later.getTime();
                return starts_after_now || starts_before_end;
            }

            EventCollection._doTheyOverlap = function (event2, event1) {
                if (event1.start <= event2.end)
                    return true;

                if (event2.start <= event1.end)
                    return 2;
            }

            /*
            Requires later to be defined. (to be the end of the timeline)
            */
            EventCollection.calculateLevels = function () {
                /*
                Implementing getLevel(): Ok, so here is the world.
                    
                Assumption: Calculating an entry's level requires O(n) lookups.
                Using a table to calculate all tables at once reduces complexity from
                O(n^2) to O(n).

                Values for i's not show are undefined at this point.
                */

                var a_level;
                // alert("size: " + EventCollection.size());
                for (var i = 0; i < EventCollection.size(); i++) {
                    if (i == 0)
                        a_level = 1;
                    // alert('i: ' + i);
                    if (i > 0 && EventCollection._eventShown(i)) {
                        // if the current event's start is before the previous (EventCollevtion) 
                        // event's end, increment the level.
                        // if the current event's start is equal to the previous event's start,
                        // increment level
                        // alert('_doTheyOverlap?\n'+'1: ' + EventCollection.get(i).start + '-' + EventCollection.get(i).end + '\n2: ' + EventCollection.get(i - 1).start + '-' + EventCollection.get(i - 1).end + '\n' + (EventCollection._doTheyOverlap(EventCollection.get(i), EventCollection.get(i - 1))?'true':'false'));
                        if (EventCollection._doTheyOverlap(EventCollection.get(i), EventCollection.get(i - 1))) {
                            a_level = EventCollection.getLevel(i - 1) + 1;
                        }
                    }
                    // alert('level: ' + a_level);
                    EventCollection.events[i][EventCollection.LEVEL_INDEX] = a_level;
                }
            }
        }

        function testEventCollection() {
            var result = true;
            later = new Date(now.getTime() + (canvas.width - (BORDER * 2)) / scale);
            new EventCollection();

            var time = new Date();
            var event1 = new Event(time.getTime() + 5000, time.getTime() + 10000, 'event1', '');
            var event2 = new Event(time.getTime() + 7000, time.getTime() + 12000, 'event2', '');

            EventCollection.push(event1);
            EventCollection.push(event1);

            alert('test_size: ' + EventCollection.size());

            EventCollection.calculateLevels();
            var level1 = EventCollection.getLevel(0);
            var level2 = EventCollection.getLevel(1);
            
            alert('test_level1: ' + level1);
            alert('test_level2: ' + level2);

            if (level1 == 1 && level2)
                return true;
            else
                return false;
        }

        function eventclicked(record) {
            var event = record.event;
            alert(event.name);
        }

        function drawText(text, x, y, event, callback) {
            var width = context.measureText(text).width;
            //context.beginPath();
            context.fillText(text, x, y);
            //context.fillRect(x, y, width, 12);
            //context.stroke();
            //context.closePath();
            var record = new RectRecord('rectangle', x, y, width, 12, event, eventclicked);
            RecordKeeper.push(record);
        }

        // no error checking!
        function displayEvent(i) {
 
            var level = EventCollection.getLevel(i);
            var LEVEL_DISTANCE = 15;
            var event = EventCollection.get(i);

            // draw the event line
            var beginX = BORDER + (event.start - now.getTime()) * scale;
            if (beginX < BORDER)
                beginX = 0;
            var beginY = canvas.height / 2 - ((level) * LEVEL_DISTANCE);

            var endX = BORDER + (event.end - now.getTime()) * scale;
            if (endX > canvas.width)
                endX = canvas.width;
            var endY = beginY;

            context.beginPath();
            context.moveTo(beginX, beginY);
            context.lineTo(endX, endY);

            context.stroke();
            context.closePath();


            // draw the text for the event
            // pushing an approximation of the rectangle onto the RecordKeeper
            var beginning = beginX;
            var middleish = beginX + (endX - beginX) / 2;
            var end = endX
            var rectX = beginning > BORDER ? beginning : (middleish > BORDER ? middleish : end)
            var rectY = beginY - 3;

            drawText(event.name, rectX, rectY, event, eventclicked);
        }

        function setup() {

            canvas = document.getElementById("canvas");
            context = canvas.getContext('2d');
            context.font = '12px mono sans serif';
            HEIGHT = canvas.height;
            WIDTH = canvas.width;
            new RecordKeeper();

            SCALE_DEFAULT = 50;
            SLIDER_MIN = 0.000001;
            SLIDER_MAX = 1;
            HALFTICKLENGTH = 10;
            document.getElementById("range").onchange = function () {
                var sc/*ale*/ = logslider(document.getElementById("range").value);
                scale = sc;
            }
            scale = logslider(document.getElementById("range").value);

            new EventCollection();

            BORDER = 10;
            now = new Date();
            var currentTime = now.getTime();

            var range = document.getElementById('range');
            range.onchange = function () {
                scale = logslider(range.value);
            }
        }

        function logslider(value) {
            var min = 0;
            var max = 100;

            var minv = Math.log(SLIDER_MIN);
            var maxv = Math.log(SLIDER_MAX);

            var sc/*ale*/ = (maxv - minv) / (max - min);

            return Math.exp(minv + sc * (value - min));
        }

        function timeclicked(record) {
            var event = record.event;
            alert(event.name);
        }

        function labelHours(now, later) {
            // determine the next full hour
            var lastHour = (now.getTime() / (1000 * 60 * 60)) * (1000 * 60 * 60);
            var nextHour = lastHour + (1000 * 60 * 60);
            for (var i = nextHour; i < later.getTime(); i += (1000 * 60 * 60)) {
                var date = new Date(i);
                var event = new Event(0, 0, date.getHours(), '');
                drawTick(i * scale, date.getHours(), event, timeclicked, 0, 0);
            }
        }

        function draw() {
            context.clearRect(0, 0, WIDTH, HEIGHT);

            RecordKeeper.clear();

            // draw timeline
            now = new Date();
            //context.strokeStyle('#000');
            context.beginPath();

            var x1 = 0, y1 = canvas.height / 2;
            var x2 = canvas.width, y2 = canvas.height / 2;
            context.moveTo(x1, y1);
            context.lineTo(x2, y2);

            context.stroke();
            context.closePath();

            // draw now tickmark
            var event = new Event(0, 0, 'now', '');
            drawTick(BORDER, now.toTimeString() + ', ' + now.toDateString(), event, timeclicked, 0, 0);

            // draw later tickmark
            context.beginPath();
            later = new Date(now.getTime() + (canvas.width - (BORDER * 2)) / scale);
            var text = later.toTimeString() + ', ' + later.toDateString();

            var event = new Event(0, 0, later.toTimeString(), '');
            drawTick(canvas.width - BORDER, text, event, timeclicked, -1 * context.measureText(text).width, 0);

            // display span next to the label
            document.getElementById("label").innerText = (later.getTime() - now.getTime()) / (1000 * 60 * 60 * 24) + " days";
            context.stroke();
            context.closePath();


            /* 
            Hourly tick marks

            Currently working on this one.
            Here are some thoughts in the meantime.
            Given two javascript Date objects, 'now' and 'later', how to draw tickmarks on every hour along the timeline.

            1. determine the lasthour = now.getTime() / (1000 * 60 * 60). This will give me the total number of hours since the epoch
            2. determine the next hour by nexthour = lasthour + (1000 * 60 * 60). This gives me the next full hour since the epoch
            3. from now on, for i=nexthour; i<later.getTime; i += (1000 * 60 * 60) drawTick(...

            4. Now for the tricky part. Scaling the timevalue into a pixel value.

            */

            // draw hourly tickmarks

            function timeToLine(time) {
                var minIn = now.getTime();
                var maxIn = later.getTime();

                var minOut = Math.log(BORDER);
                var maxOut = Math.log(canvas.width - BORDER * 2);

                var sc/*ale*/ = (maxOut - minOut) / (maxIn - minIn);

                return Math.exp(minOut + sc * (time - minIn));
            }

            var msInHour = 1000 * 60 * 60;

            var lastHour = now.getTime() / msInHour * msInHour; // yay integer division
            var nextHour = lastHour + msInHour;
            for (var i = nextHour; i < later.getTime(); i += msInHour) {
                var hour = new Date(i);
                var event = new Event(0, 0, hour.getHours(), '');
                drawTick(timeToLine(i), hour.getHours(), event, timeclicked, 0, 0);
            }


            // draw events
            EventCollection.calculateLevels();
            for (var i = 0; i < EventCollection.size(); i++) {
                if (EventCollection.get(i).end < now.getTime()) {
                    EventCollection.remove(i);
                    return;
                } else {
                    displayEvent(i);
                }
            }
        }



    </script>


    <script type="text/javascript" src="http://www.google.com/jsapi"></script>
    <script type="text/javascript">
        google.load('gdata', '2.x');
        google.load('jquery', '1.4.4');


        function GCalenderEventToTimelineEvent(entry) {
            var times = entry.getTimes();
            var start = times.startTime;
            var end = times.endTime;
        }

        function onLoad(result) {
            var entries = result.feed.getEntries();
            for (var i = 0; i < entries.length; i++) {
                var entry = entries[i];
                var something = entry.getTimes();
                var start = Date.parse(something[0].startTime);
                var end = Date.parse(something[0].endTime);

                var event = new Event(start, end, entry.getTitle().getText(), entry.getSelfLink());
                EventCollection.push(event);
                //console.log(JSON.stringify(event));
            }
        }

        function onError(result) {
            alert("What we've got here, is a failure, to communicate.");
        }

        google.setOnLoadCallback(function () {
            var status = google.accounts.user.getStatus();

            if (status == google.accounts.AuthSubStatus.LOGGING_IN)
                return;
            else if (status == google.accounts.AuthSubStatus.LOGGED_OUT) {
                return; // display login
            } else {
                service = new google.gdata.calendar.CalendarService('jmm-timeline-js');
                var feedUrl = 'http://www.google.com/calendar/feeds/default/private/full';

                setup(); // defines global variables now, EventCollection

                function pad(int) {
                    if (int < 10)
                        return '0' + int;
                }

                run_test(testEventCollection);

                var query = new google.gdata.calendar.CalendarEventQuery(feedUrl);
                query.setOrderBy('starttime');
                query.setSortOrder('ascending');
                query.setFutureEvents(true);
                query.setSingleEvents(true);
                query.setMaxResults(5);
                service.getEventsFeed(query, onLoad, onError);

                setInterval('draw()', 30);
            }
        });
    </script>

</html>